查看原文
其他

Cocos Creator 的 3D 寻路实现探索:NavMesh 导航网格

搬砖小菜鸟 COCOS 2022-06-10

「寻路」是很多游戏不可或缺的功能。2D 游戏中最常用的就是 A* 寻路了。而在 3D 游戏中,对于一些简单的、没有高度的地面,A* 寻路也是可以使用的;但是对于一些地面比较复杂的游戏,寻路功能怎样实现比较好呢?


本文将分析 RecastNavigation、耳切法 +A*、NavMesh 导航这三种方案,找找效果最佳、最适合 Cocos Creator 的寻路实现方案。


效果展示


RecastNavigation


相信了解过 3D 寻路的小伙伴都有听说过 RecastNavigation。RecastNavigation 是一款非常强大的寻路系统,被广泛的应用于各大主流游戏引擎中。


NavMesh 的生成基本原理:

  1. 体素化。从源几何体构造实心的高度场,用来表示不可行走的空间。

  2. 生成地区。将实心高度场的上表面中连续的区间合并为地区。

  3. 生成轮廓。检测地区的轮廓,并构造成简单多边形。

  4. 生成多边形网格。将轮廓分割成凸多边形。

  5. 生成高度细节。将多边形网格三角化,得到高度细节。


感兴趣的小伙伴们可以去了解一下,甚至可以去了解了解源码。


耳切法 +A*


了解了导航寻路的原理后,我认为寻路的关键分为两部分:网格画与寻路



我们先从最简单的没有高度的地图入手,大概步骤如下:

  1. 基于耳切法实现地图的网格化;

  2. 基于 A* 实现寻路;

  3. 基于漏斗算法将三角形集合进行路径最优解。



经过尝试,基本的寻路功能是实现了,但是在路径的优化方面会存在一些问题,所以我决定去寻找一些现成的 js 库来实现这个功能。


NavMesh 导航


在 github 闲逛的时候发现了 recast.js 这个库,于是了解了一下它的具体功能和 API。花了点时间,成功将 recast.js 移到了 Cocos Creator 3.x。



接着尝试用 recast.js 实现寻路。


初始化


import Recast from "./lib/recast.js"
...
public init(cb: Function = null):void{
  new Recast().then((recast) => {
      this._recast = recast;
      this._navMesh = new this._recast.NavMesh();
      this.setDefaultConfig();

      this._tempVec=new this._recast.Vec3();
      this._tempVec1=new this._recast.Vec3();
      this._tempVec2=new this._recast.Vec3();
      if (cb) cb();
  });
}


添加静态物体


/**
 * 添加静态的模型
 */

public addStaticModle(node: Node): void {
    if(!node||!node.getComponent(MeshRenderer)) return;
    let render: MeshRenderer = node.getComponent(MeshRenderer);
    if (!render || !render.mesh) return;
    let matrix: math.Mat4 = node.getWorldMatrix();
    this.updateBaseDatas(render.mesh, matrix);
}


场景中一些固定不变的,比如地面、障碍物之类的可以通过这个结构进行添加,代码中对传入节点的网格信息进行处理,处理为 recast 所需要的格式。



添加 Terrain 地形


 /**
 * 添加cocos的地形 
 * @param terrain 地形组件
 */

public addTerrain (terrain: Terrain,): any {
   ...
}


terrain 地形组件同时也是游戏开发过程中会常用到的组件。听取了大佬的建议,将咱们的寻路对 terrain 进行了支持,大家可以在 Demo 中看见对 terrain 数据处理的一些逻辑。



构建导航网格


/**
 * 构建导航网格
 */

public build(): void {
    var rc = new this._recast.rcConfig();
    rc.cs = this._config.cs;
    rc.ch = this._config.ch;
    ....
    this._navMesh.build(this._positions, this._positions.length / 3this._indices, this._indices.length, rc);
}


可视化调试


在构建完导航网格后,为了方便调试,将最终的数据以可视化的效果进行展现。代码中已经做好了处理,提供了两种调试方式:

export enum MeshDebugDataType {
    SURFACE = 0//面
    LINE = 1,   //线
}

let navMeshData: NavMeshDebugData = this._navMeshMgr.getNavMeshDebugData(MeshDebugDataType.LINE);
this.createDebugMesh(navMeshData.positions, navMeshData.normals);

private createDebugMesh (positions: number[], normals: number[]): void {
    this.line.node.destroyAllChildren();
    let node = new Node();
    let render = node.addComponent(MeshRenderer);
    let mesh = utils.createMesh({
        positions: positions,
        primitiveMode: gfx.PrimitiveMode.TRIANGLE_LIST,
        normals: normals,
    });
    render.mesh = mesh;
    this.line.node.addChild(node);
}



Crowd 和 Agent


首先谈谈本人对这两个概念的理解吧:

  • Crowd,从字面意识大概就能猜到,即人群。人群可以有多个。

  • Agent,表示的是这群人里边的其中一个。

我推测每个 Crowd 中的 Agent 在移动的时候会进行彼此之间距离的一些检测,但是不同 Crowd 的 Agent 则不会检测(还未验证)。


首先创建 Crowd:

public initCrowd (maxAgents: number, maxAgentRadius: number): NavMeshCorwd {
    return new NavMeshCorwd(this, maxAgents, maxAgentRadius);
}


添加一个 Agent:


public addAgent (pos: Vec3, parameters: AgentConfig): number {
    let config = new this._navMeshMgr.recast.dtCrowdAgentParams();
    ...
    let agentIndex: number = this._recastCrowd.addAgent(new this._navMeshMgr.recast.Vec3(pos.x, pos.y, pos.z), config);
    ...
    return agentIndex;
}


寻路移动


导航网格的关键点就是为了获取两点之间的最有路径。

private findPath (pos): void {
    let targetPos: Vec3 = this._navMeshMgr.getClosestPoint(pos);
    //设置指定的Agent移动到特定位置
    this._navMeshCrowd.agentMoveTarget(this._playerAgentIndex, targetPos);
}

通过 agentMoveTarget() 函数去置顶 Agent 的目标点,同时还需要去实时刷新 Crowd 来更新 Agnet 的实际位置。

 update (deltaTime: number) {
    if(! this._navMeshCrowd) return;
    //刷新Crowd
    this._navMeshCrowd.update(deltaTime);
    //获取指定Agent当前的位置
    let agentPosition = this._navMeshCrowd.getAgentPosition(this._playerAgentIndex);
    this.player.position = agentPosition;
    }





希望以上内容能对小伙伴们有所帮助。点击文末【阅读原文】前往 Cocos Store 下载 Demo。


本文作者为搬砖小菜鸟

欢迎关注他的个人公众号与他交流↓


往期精彩

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存